# (C) British Crown Copyright 2010 - 2017, Met Office
#
# This file is part of Iris.
#
# Iris is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the
# Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Iris is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Iris.  If not, see <http://www.gnu.org/licenses/>.
"""
Provides UK Met Office Fields File (FF) format specific capabilities.

"""

from __future__ import (absolute_import, division, print_function)
from six.moves import (filter, input, map, range, zip)  # noqa

import os
import warnings

import numpy as np

from iris.exceptions import NotYetImplementedError
from iris.fileformats._ff_cross_references import STASH_TRANS
from . import pp


IMDI = -32768

FF_HEADER_DEPTH = 256      # In words (64-bit).
DEFAULT_FF_WORD_DEPTH = 8  # In bytes.

# UM marker to signify empty lookup table entry.
_FF_LOOKUP_TABLE_TERMINATE = -99

# UM FieldsFile fixed length header names and positions.
UM_FIXED_LENGTH_HEADER = [
    ('data_set_format_version',    (1, )),
    ('sub_model',                  (2, )),
    ('vert_coord_type',            (3, )),
    ('horiz_grid_type',            (4, )),
    ('dataset_type',               (5, )),
    ('run_identifier',             (6, )),
    ('experiment_number',          (7, )),
    ('calendar',                   (8, )),
    ('grid_staggering',            (9, )),
    ('time_type',                  (10, )),
    ('projection_number',          (11, )),
    ('model_version',              (12, )),
    ('obs_file_type',              (14, )),
    ('last_fieldop_type',          (15, )),
    ('first_validity_time',        (21, 22, 23, 24, 25, 26, 27, )),
    ('last_validity_time',         (28, 29, 30, 31, 32, 33, 34, )),
    ('misc_validity_time',         (35, 36, 37, 38, 39, 40, 41, )),
    ('integer_constants',          (100, 101, )),
    ('real_constants',             (105, 106, )),
    ('level_dependent_constants',  (110, 111, 112, )),
    ('row_dependent_constants',    (115, 116, 117, )),
    ('column_dependent_constants', (120, 121, 122, )),
    ('fields_of_constants',        (125, 126, 127, )),
    ('extra_constants',            (130, 131, )),
    ('temp_historyfile',           (135, 136, )),
    ('compressed_field_index1',    (140, 141, )),
    ('compressed_field_index2',    (142, 143, )),
    ('compressed_field_index3',    (144, 145, )),
    ('lookup_table',               (150, 151, 152, )),
    ('total_prognostic_fields',    (153, )),
    ('data',                       (160, 161, 162, )), ]

# Offset value to convert from UM_FIXED_LENGTH_HEADER positions to
# FF_HEADER offsets.
UM_TO_FF_HEADER_OFFSET = 1
# Offset the UM_FIXED_LENGTH_HEADER positions to FF_HEADER offsets.
FF_HEADER = [
    (name, tuple(position - UM_TO_FF_HEADER_OFFSET for position in positions))
    for name, positions in UM_FIXED_LENGTH_HEADER]

# UM FieldsFile fixed length header pointer names.
_FF_HEADER_POINTERS = [
    'integer_constants',
    'real_constants',
    'level_dependent_constants',
    'row_dependent_constants',
    'column_dependent_constants',
    'fields_of_constants',
    'extra_constants',
    'temp_historyfile',
    'compressed_field_index1',
    'compressed_field_index2',
    'compressed_field_index3',
    'lookup_table',
    'data', ]

_LBUSER_DTYPE_LOOKUP = {1: '>f{word_depth}',
                        2: '>i{word_depth}',
                        3: '>i{word_depth}',
                        'default': '>f{word_depth}', }

#: Codes used in STASH_GRID which indicate the x coordinate is on the
#: edge of the cell.
X_COORD_U_GRID = (11, 18, 27)

#: Codes used in STASH_GRID which indicate the y coordinate is on the
#: edge of the cell.
Y_COORD_V_GRID = (11, 19, 28)

#: Grid codes found in the STASH master which are currently known to be
#: handled correctly. A warning is issued if a grid is found which is not
#: handled.
HANDLED_GRIDS = (1, 2, 3, 4, 5, 21, 26, 29) + X_COORD_U_GRID + Y_COORD_V_GRID

# REAL constants header names as described by UM documentation paper F3.
# NB. These are zero-based indices as opposed to the one-based indices
# used in F3.
REAL_EW_SPACING = 0
REAL_NS_SPACING = 1
REAL_FIRST_LAT = 2
REAL_FIRST_LON = 3
REAL_POLE_LAT = 4
REAL_POLE_LON = 5


class Grid(object):
    """
    An abstract class representing the default/file-level grid
    definition for a FieldsFile.

    """
    def __init__(self, column_dependent_constants, row_dependent_constants,
                 real_constants, horiz_grid_type):
        """
        Create a Grid from the relevant sections of the FFHeader.

        Args:

        * column_dependent_constants (numpy.ndarray):
            The `column_dependent_constants` from a FFHeader.

        * row_dependent_constants (numpy.ndarray):
            The `row_dependent_constants` from a FFHeader.

        * real_constants (numpy.ndarray):
            The `real_constants` from a FFHeader.

        * horiz_grid_type (integer):
            `horiz_grid_type` from a FFHeader.

        """
        self.column_dependent_constants = column_dependent_constants
        self.row_dependent_constants = row_dependent_constants
        self.ew_spacing = real_constants[REAL_EW_SPACING]
        self.ns_spacing = real_constants[REAL_NS_SPACING]
        self.first_lat = real_constants[REAL_FIRST_LAT]
        self.first_lon = real_constants[REAL_FIRST_LON]
        self.pole_lat = real_constants[REAL_POLE_LAT]
        self.pole_lon = real_constants[REAL_POLE_LON]
        self.horiz_grid_type = horiz_grid_type

    def _x_vectors(self, subgrid):
        # Abstract method to return the X vector for the given sub-grid.
        raise NotImplementedError()

    def _y_vectors(self, subgrid):
        # Abstract method to return the X vector for the given sub-grid.
        raise NotImplementedError()

    def regular_x(self, subgrid):
        # Abstract method to return BZX, BDX for the given sub-grid.
        raise NotImplementedError()

    def regular_y(self, subgrid):
        # Abstract method to return BZY, BDY for the given sub-grid.
        raise NotImplementedError()

    def vectors(self, subgrid):
        """
        Return the X and Y coordinate vectors for the given sub-grid of
        this grid.

        Args:

        * subgrid (integer):
            A "grid type code" as described in UM documentation paper C4.

        Returns:
            A 2-tuple of X-vector, Y-vector.

        """
        x_p, x_u = self._x_vectors()
        y_p, y_v = self._y_vectors()
        x = x_p
        y = y_p
        if subgrid in X_COORD_U_GRID:
            x = x_u
        if subgrid in Y_COORD_V_GRID:
            y = y_v
        return x, y


class ArakawaC(Grid):
    """
    An abstract class representing an Arakawa C-grid.

    """
    def _x_vectors(self):
        x_p, x_u = None, None
        if self.column_dependent_constants is not None:
            x_p = self.column_dependent_constants[:, 0]
            if self.column_dependent_constants.shape[1] == 2:
                # Wrap around for global field
                if self.horiz_grid_type == 0:
                    x_u = self.column_dependent_constants[:-1, 1]
                else:
                    x_u = self.column_dependent_constants[:, 1]
        return x_p, x_u

    def regular_x(self, subgrid):
        """
        Return the "zeroth" value and step for the X coordinate on the
        given sub-grid of this grid.

        Args:

        * subgrid (integer):
            A "grid type code" as described in UM documentation paper C4.

        Returns:
            A 2-tuple of BZX, BDX.

        """
        bdx = self.ew_spacing
        bzx = self.first_lon - bdx
        if subgrid in X_COORD_U_GRID:
            bzx += 0.5 * bdx
        return bzx, bdx

    def regular_y(self, subgrid):
        """
        Return the "zeroth" value and step for the Y coordinate on the
        given sub-grid of this grid.

        Args:

        * subgrid (integer):
            A "grid type code" as described in UM documentation paper C4.

        Returns:
            A 2-tuple of BZY, BDY.

        """
        bdy = self.ns_spacing
        bzy = self.first_lat - bdy
        if subgrid in Y_COORD_V_GRID:
            bzy += self._v_offset * bdy
        return bzy, bdy


class NewDynamics(ArakawaC):
    """
    An Arakawa C-grid as used by UM New Dynamics.

    The theta and u points are at the poles.

    """

    _v_offset = 0.5

    def _y_vectors(self):
        y_p, y_v = None, None
        if self.row_dependent_constants is not None:
            y_p = self.row_dependent_constants[:, 0]
            if self.row_dependent_constants.shape[1] == 2:
                y_v = self.row_dependent_constants[:-1, 1]
        return y_p, y_v


class ENDGame(ArakawaC):
    """
    An Arakawa C-grid as used by UM ENDGame.

    The v points are at the poles.

    """

    _v_offset = -0.5

    def _y_vectors(self):
        y_p, y_v = None, None
        if self.row_dependent_constants is not None:
            y_p = self.row_dependent_constants[:-1, 0]
            if self.row_dependent_constants.shape[1] == 2:
                y_v = self.row_dependent_constants[:, 1]
        return y_p, y_v


class FFHeader(object):
    """
    A class to represent the FIXED_LENGTH_HEADER section of a FieldsFile.

    """

    GRID_STAGGERING_CLASS = {3: NewDynamics, 6: ENDGame}

    def __init__(self, filename, word_depth=DEFAULT_FF_WORD_DEPTH):
        """
        Create a FieldsFile header instance by reading the
        FIXED_LENGTH_HEADER section of the FieldsFile, making the names
        defined in FF_HEADER available as attributes of a FFHeader instance.

        Args:

        * filename (string):
            Specify the name of the FieldsFile.

        Returns:
            FFHeader object.

        """

        #: File name of the FieldsFile.
        self.ff_filename = filename
        self._word_depth = word_depth

        # Read the FF header data
        with open(filename, 'rb') as ff_file:
            # typically 64-bit words (aka. int64 or ">i8")
            header_data = np.fromfile(ff_file,
                                      dtype='>i{0}'.format(word_depth),
                                      count=FF_HEADER_DEPTH)
            header_data = tuple(header_data)
            # Create FF instance attributes
            for name, offsets in FF_HEADER:
                if len(offsets) == 1:
                    value = header_data[offsets[0]]
                else:
                    value = header_data[offsets[0]:offsets[-1] + 1]
                setattr(self, name, value)

            # Turn the pointer values into real arrays.
            for elem in _FF_HEADER_POINTERS:
                if elem not in ['data', 'lookup_table']:
                    if self._attribute_is_pointer_and_needs_addressing(elem):
                        addr = getattr(self, elem)
                        ff_file.seek((addr[0] - 1) * word_depth, os.SEEK_SET)
                        if len(addr) == 2:
                            if elem == 'integer_constants':
                                res = np.fromfile(
                                    ff_file,
                                    dtype='>i{0}'.format(word_depth),
                                    count=addr[1])
                            else:
                                res = np.fromfile(
                                    ff_file,
                                    dtype='>f{0}'.format(word_depth),
                                    count=addr[1])
                        elif len(addr) == 3:
                            res = np.fromfile(ff_file,
                                              dtype='>f{0}'.format(word_depth),
                                              count=addr[1]*addr[2])
                            res = res.reshape((addr[1], addr[2]), order='F')
                        else:
                            raise ValueError('ff header element {} is not'
                                             'handled correctly'.format(elem))
                    else:
                        res = None
                    setattr(self, elem, res)

    def __str__(self):
        attributes = []
        for name, _ in FF_HEADER:
            attributes.append('    {}: {}'.format(name, getattr(self, name)))
        return 'FF Header:\n' + '\n'.join(attributes)

    def __repr__(self):
        return '{}({!r})'.format(type(self).__name__, self.ff_filename)

    def _attribute_is_pointer_and_needs_addressing(self, name):
        if name in _FF_HEADER_POINTERS:
            attr = getattr(self, name)

            # Check that we haven't already addressed this pointer,
            # that the pointer is actually referenceable (i.e. >0)
            # and that the attribute is not marked as missing.
            is_referenceable = (isinstance(attr, tuple) and
                                attr[0] > 0 and attr[0] != IMDI)
        else:
            msg = '{!r} object does not have pointer attribute {!r}'
            raise AttributeError(msg.format(self.__class__.__name__, name))
        return is_referenceable

    def shape(self, name):
        """
        Return the dimension shape of the FieldsFile FIXED_LENGTH_HEADER
        pointer attribute.

        Args:

        * name (string):
            Specify the name of the FIXED_LENGTH_HEADER attribute.

        Returns:
            Dimension tuple.

        """

        if name in _FF_HEADER_POINTERS:
            value = getattr(self, name)[1:]
        else:
            msg = '{!r} object does not have pointer address {!r}'
            raise AttributeError(msg.format(self.__class_.__name__, name))
        return value

    def grid(self):
        """Return the Grid definition for the FieldsFile."""
        grid_class = self.GRID_STAGGERING_CLASS.get(self.grid_staggering)
        if grid_class is None:
            grid_class = NewDynamics
            warnings.warn(
                'Staggered grid type: {} not currently interpreted, assuming '
                'standard C-grid'.format(self.grid_staggering))
        grid = grid_class(self.column_dependent_constants,
                          self.row_dependent_constants,
                          self.real_constants, self.horiz_grid_type)
        return grid


class FF2PP(object):
    """
    A class to extract the individual PPFields from within a FieldsFile.

    """

    def __init__(self, filename, read_data=False,
                 word_depth=DEFAULT_FF_WORD_DEPTH):
        """
        Create a FieldsFile to Post Process instance that returns a generator
        of PPFields contained within the FieldsFile.

        Args:

        * filename (string):
            Specify the name of the FieldsFile.

        Kwargs:

        * read_data (boolean):
            Specify whether to read the associated PPField data within
            the FieldsFile.  Default value is False.

        Returns:
            PPField generator.

        For example::

            >>> for field in ff.FF2PP(filename):
            ...     print(field)

        """

        self._ff_header = FFHeader(filename, word_depth=word_depth)
        self._word_depth = word_depth
        self._filename = filename
        self._read_data = read_data

    def _payload(self, field):
        """Calculate the payload data depth (in bytes) and type."""
        lbpack_n1 = field.raw_lbpack % 10
        if lbpack_n1 == 0:
            word_depth = self._word_depth
            # Data payload is not packed.
            data_words = field.lblrec - field.lbext
            # Determine PP field 64-bit payload datatype.
            lookup = _LBUSER_DTYPE_LOOKUP
            dtype_template = lookup.get(field.lbuser[0], lookup['default'])
            dtype_name = dtype_template.format(word_depth=self._word_depth)
            data_type = np.dtype(dtype_name)
        else:
            word_depth = pp.PP_WORD_DEPTH
            # Data payload is packed.
            if lbpack_n1 == 1:
                # Data packed using WGDOS archive method.
                data_words = field.lbnrec * 2
            elif lbpack_n1 == 2:
                # Data packed using CRAY 32-bit method.
                data_words = field.lblrec - field.lbext
            else:
                msg = 'PP fields with LBPACK of {} are not supported.'
                raise NotYetImplementedError(msg.format(field.raw_lbpack))

            # Determine PP field payload datatype.
            lookup = pp.LBUSER_DTYPE_LOOKUP
            data_type = lookup.get(field.lbuser[0], lookup['default'])

        if field.boundary_packing is not None:
            if lbpack_n1 not in (0, 2):
                # Can't handle the usual packing methods with LBC data.
                raise ValueError(
                    'LBC data has LBPACK = {:d}, but packed LBC data is not '
                    'supported.'.format(field.raw_lbpack))
            # Adjust to packed data size, for LBC data.
            # NOTE: logic duplicates that in pp._data_bytes_to_shaped_array.
            pack_dims = field.boundary_packing
            boundary_height = pack_dims.y_halo + pack_dims.rim_width
            boundary_width = pack_dims.x_halo + pack_dims.rim_width
            y_height, x_width = field.lbrow, field.lbnpt
            mid_height = y_height - 2 * boundary_height
            data_words = (boundary_height * x_width * 2 +
                          boundary_width * mid_height * 2)

        data_depth = data_words * word_depth
        return data_depth, data_type

    def _det_border(self, field_dim, halo_dim):
        # Update field coordinates for a variable resolution LBC file where
        # the resolution of the very edge (within the rim width) is assumed to
        # be same as the halo.
        def range_order(range1, range2, resolution):
            # Handles whether increasing/decreasing ranges.
            if np.sign(resolution) > 0:
                lower = range1
                upper = range2
            else:
                upper = range1
                lower = range2
            return lower, upper

        # Ensure that the resolution is the same on both edges.
        res_low = field_dim[1] - field_dim[0]
        res_high = field_dim[-1] - field_dim[-2]
        if not np.allclose(res_low, res_high):
            msg = ('The x or y coordinates of your boundary condition field '
                   'may be incorrect, not having taken into account the '
                   'boundary size.')
            warnings.warn(msg)
        else:
            range2 = field_dim[0] - res_low
            range1 = field_dim[0] - halo_dim * res_low
            lower, upper = range_order(range1, range2, res_low)
            extra_before = np.linspace(lower, upper, halo_dim)

            range1 = field_dim[-1] + res_high
            range2 = field_dim[-1] + halo_dim * res_high
            lower, upper = range_order(range1, range2, res_high)
            extra_after = np.linspace(lower, upper, halo_dim)

            field_dim = np.concatenate([extra_before, field_dim, extra_after])
        return field_dim

    def _adjust_field_for_lbc(self, field):
        """
        Make an LBC field look like a 'normal' field for rules processing.

        """
        # Set LBTIM to indicate the specific time encoding for LBCs,
        # i.e. t1=forecast, t2=reference
        lbtim_default = 11
        if field.lbtim not in (0, lbtim_default):
            raise ValueError('LBC field has LBTIM of {:d}, expected only '
                             '0 or {:d}.'.format(field.lbtim, lbtim_default))
        field.lbtim = lbtim_default

        # Set LBVC to indicate the specific height encoding for LBCs,
        # i.e. hybrid height layers.
        lbvc_default = 65
        if field.lbvc not in (0, lbvc_default):
            raise ValueError('LBC field has LBVC of {:d}, expected only '
                             '0 or {:d}.'.format(field.lbvc, lbvc_default))
        field.lbvc = lbvc_default
        # Specifying a vertical encoding scheme means a usable vertical
        # coordinate can be produced, because we also record the level
        # number in each result field:  Thus they are located, and can
        # be stacked, in a vertical dimension.
        # See per-layer loop (below).

        # Calculate field packing details.
        name_mapping = dict(rim_width=slice(4, 6), y_halo=slice(2, 4),
                            x_halo=slice(0, 2))
        boundary_packing = pp.SplittableInt(field.lbuser[2], name_mapping)
        # Store packing on the field, which affects how it gets the data.
        field.boundary_packing = boundary_packing
        # Fix the lbrow and lbnpt to be the actual size of the data
        # array, since the field is no longer a "boundary" fields file
        # field.
        # Note: The documentation states that lbrow (y) doesn't
        # contain the halo rows, but no such comment exists at UM v8.5
        # for lbnpt (x). Experimentation has shown that lbnpt also
        # excludes the halo size.
        field.lbrow += 2 * boundary_packing.y_halo
        field.lbnpt += 2 * boundary_packing.x_halo

        # Update the x and y coordinates for this field. Note: it may
        # be that this needs to update x and y also, but that is yet
        # to be confirmed.
        if (field.bdx in (0, field.bmdi) or
                field.bdy in (0, field.bmdi)):
            field.x = self._det_border(field.x, boundary_packing.x_halo)
            field.y = self._det_border(field.y, boundary_packing.y_halo)
        else:
            if field.bdy < 0:
                warnings.warn('The LBC has a bdy less than 0. No '
                              'case has previously been seen of '
                              'this, and the decompression may be '
                              'erroneous.')
            field.bzx -= field.bdx * boundary_packing.x_halo
            field.bzy -= field.bdy * boundary_packing.y_halo

    def _fields_over_all_levels(self, field):
        """
        Replicate the field over all model levels, setting LBLEV for each.

        This is appropriate for LBC data.
        Yields an iterator producing a sequence of distinct field objects.

        """
        n_all_levels = self._ff_header.level_dependent_constants.shape[0]
        levels_count = field.lbhem - 100
        if levels_count < 1:
            raise ValueError(
                'LBC field has LBHEM of {:d}, but this should be (100 '
                '+ levels-per-field-type), hence >= 101.'.format(field.lbhem))
        if levels_count > n_all_levels:
            raise ValueError(
                'LBC field has LBHEM of (100 + levels-per-field-type) '
                '= {:d}, but this is more than the total number of levels '
                'in the file = {:d}).'.format(field.lbhem, n_all_levels))

        for i_model_level in range(levels_count):
            # Make subsequent fields alike, but distinct.
            if i_model_level > 0:
                field = field.copy()
            # Provide the correct "model level" value.
            field.lblev = i_model_level
            # TODO: as LBC lookup headers cover multiple layers, they
            # contain no per-layer height values, which are all 0.
            # So the field's "height" coordinate will be useless.
            # It should be possible to fix this here, by calculating
            # per-layer Z and C values from the file header, and
            # setting blev/brlev/brsvd1 and bhlev/bhrlev/brsvd2 here,
            # but we don't yet do this.

            yield field

    def _extract_field(self):
        # FF table pointer initialisation based on FF LOOKUP table
        # configuration.

        lookup_table = self._ff_header.lookup_table
        table_index, table_entry_depth, table_count = lookup_table
        table_offset = (table_index - 1) * self._word_depth       # in bytes
        table_entry_depth = table_entry_depth * self._word_depth  # in bytes
        # Open the FF for processing.
        with open(self._ff_header.ff_filename, 'rb') as ff_file:
            ff_file_seek = ff_file.seek

            is_boundary_packed = self._ff_header.dataset_type == 5

            grid = self._ff_header.grid()

            # Process each FF LOOKUP table entry.
            while table_count:
                table_count -= 1
                # Move file pointer to the start of the current FF LOOKUP
                # table entry.
                ff_file_seek(table_offset, os.SEEK_SET)

                # Read the current PP header entry from the FF LOOKUP table.
                header_longs = np.fromfile(
                    ff_file, dtype='>i{0}'.format(self._word_depth),
                    count=pp.NUM_LONG_HEADERS)
                # Check whether the current FF LOOKUP table entry is valid.
                if header_longs[0] == _FF_LOOKUP_TABLE_TERMINATE:
                    # There are no more FF LOOKUP table entries to read.
                    break
                header_floats = np.fromfile(
                    ff_file, dtype='>f{0}'.format(self._word_depth),
                    count=pp.NUM_FLOAT_HEADERS)
                header = tuple(header_longs) + tuple(header_floats)

                # Calculate next FF LOOKUP table entry.
                table_offset += table_entry_depth

                # Construct a PPField object and populate using the header_data
                # read from the current FF LOOKUP table.
                # (The PPField sub-class will depend on the header release
                # number.)
                # (Some Fields File fields are UM specific scratch spaces, with
                # no header release number, these will throw an exception from
                # the PP module and are skipped to enable reading of the file.
                try:
                    field = pp.make_pp_field(header)

                    # Fast stash look-up.
                    stash_s = field.lbuser[3] // 1000
                    stash_i = field.lbuser[3] % 1000
                    stash = 'm{:02}s{:02}i{:03}'.format(field.lbuser[6],
                                                        stash_s, stash_i)
                    stash_entry = STASH_TRANS.get(stash, None)
                    if stash_entry is None:
                        subgrid = None
                    else:
                        subgrid = stash_entry.grid_code
                        if subgrid not in HANDLED_GRIDS:
                            warnings.warn('The stash code {} is on a grid {} '
                                          'which has not been explicitly '
                                          'handled by the fieldsfile loader.'
                                          ' Assuming the data is on a P grid'
                                          '.'.format(stash, subgrid))

                    field.x, field.y = grid.vectors(subgrid)

                    # Use the per-file grid if no per-field metadata is
                    # available.
                    no_x = field.bzx in (0, field.bmdi) and field.x is None
                    no_y = field.bzy in (0, field.bmdi) and field.y is None
                    if no_x and no_y:
                        if subgrid is None:
                            msg = ('The STASH code {0} was not found in the '
                                   'STASH to grid type mapping. Picking the P '
                                   'position as the cell type'.format(stash))
                            warnings.warn(msg)
                        field.bzx, field.bdx = grid.regular_x(subgrid)
                        field.bzy, field.bdy = grid.regular_y(subgrid)
                        field.bplat = grid.pole_lat
                        field.bplon = grid.pole_lon
                    elif no_x or no_y:
                        warnings.warn(
                            'Partially missing X or Y coordinate values.')

                    # Check for LBC fields.
                    is_boundary_packed = self._ff_header.dataset_type == 5
                    if is_boundary_packed:
                        # Apply adjustments specific to LBC data.
                        self._adjust_field_for_lbc(field)

                    # Calculate start address of the associated PP header data.
                    data_offset = field.lbegin * self._word_depth
                    # Determine PP field payload depth and type.
                    data_depth, data_type = self._payload(field)

                    # Produce (yield) output fields.
                    if is_boundary_packed:
                        fields = self._fields_over_all_levels(field)
                    else:
                        fields = [field]
                    for result_field in fields:
                        # Add a field data element.
                        if self._read_data:
                            # Read the actual bytes. This can then be converted
                            # to a numpy array at a higher level.
                            ff_file_seek(data_offset, os.SEEK_SET)
                            result_field.data = pp.LoadedArrayBytes(
                                ff_file.read(data_depth), data_type)
                        else:
                            # Provide enough context to read the data bytes
                            # later.
                            result_field.data = (self._filename, data_offset,
                                                 data_depth, data_type)

                        data_offset += data_depth

                        yield result_field
                except ValueError as valerr:
                    msg = ('Input field skipped as PPField creation failed :'
                           ' error = {!r}')
                    warnings.warn(msg.format(str(valerr)))

    def __iter__(self):
        return pp._interpret_fields(self._extract_field())


def load_cubes(filenames, callback, constraints=None):
    """
    Loads cubes from a list of fields files filenames.

    Args:

    * filenames - list of fields files filenames to load

    Kwargs:

    * callback - a function which can be passed on to
        :func:`iris.io.run_callback`

    .. note::

        The resultant cubes may not be in the order that they are in the
        file (order is not preserved when there is a field with
        orography references).

    """
    return pp._load_cubes_variable_loader(filenames, callback, FF2PP,
                                          constraints=constraints)


def load_cubes_32bit_ieee(filenames, callback, constraints=None):
    """
    Loads cubes from a list of 32bit ieee converted fieldsfiles filenames.

    .. seealso::

        :func:`load_cubes` for keyword details

    """
    return pp._load_cubes_variable_loader(filenames, callback, FF2PP,
                                          {'word_depth': 4},
                                          constraints=constraints)
